ExpressBeans   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 162
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 23
eloc 93
dl 0
loc 162
ccs 55
cts 55
cp 1
rs 10
c 0
b 0
f 0

8 Functions

Rating   Name   Duplication   Size   Complexity  
A registerRouters 0 24 2
A createApp 0 8 1
C serializeRequest 0 16 10
A checkRouterBeans 0 17 2
A getApp 0 7 1
A use 0 7 1
A initialize 0 27 3
A listen 0 14 3
1 5
import express, { Express, Request, Router } from 'express';
2 5
import { pinoHttp, startTime } from 'pino-http';
3
import { ServerResponse, IncomingMessage } from 'http';
4 5
import EventEmitter from 'events';
5
import { ExpressBeansOptions, ExpressRouterBean } from '@/ExpressBeansTypes';
6 5
import { logger } from '@/core';
7 5
import { Executor } from '@/core/Executor';
8
9
type ExpressBeanEventMap = {
10
  error: [Error];
11
  initialized: [];
12
}
13
14 5
export default class ExpressBeans extends EventEmitter<ExpressBeanEventMap> {
15 22
  private readonly app: Express;
16
17 22
  private readonly router: Router;
18
19
  /**
20
   * Creates a new ExpressBeans application
21
   * @param options {ExpressBeansOptions}
22
   */
23
  static async createApp(options?: Partial<ExpressBeansOptions>): Promise<ExpressBeans> {
24 6
    const app = new ExpressBeans({ ...options });
25 6
    return app;
26
  }
27
28
  /**
29
   * Constructor of ExpressBeans application
30
   * @constructor
31
   * @param options {ExpressBeansOptions}
32
   */
33
  constructor(options?: Partial<ExpressBeansOptions>) {
34 22
    super();
35 22
    this.router = express.Router();
36 22
    this.app = express();
37 22
    this.app.disable('x-powered-by');
38 22
    this.app.use(options?.baseURL ?? '/', this.router);
39 22
    if (options?.logRequests === undefined || options.logRequests) {
40 20
      this.router.use(pinoHttp(
41
        {
42
          logger,
43
          customSuccessMessage: this.serializeRequest.bind(this),
44
          customErrorMessage: this.serializeRequest.bind(this),
45
        },
46
      ));
47
    }
48 22
    Executor.setExecution('init', async () => this.initialize(options ?? {}));
49 22
    Executor.eventEmitter.on('error', () => {
50 6
      process.exit(1);
51
    });
52 22
    Executor.startLifecycle();
53
  }
54
55
  private serializeRequest(req: IncomingMessage, res: ServerResponse) {
56 12
    const request: Request = req as Request;
57 12
    const remoteAddress = request.headers['x-forwarded-for'] ?? request.socket.remoteAddress;
58 12
    const { method, originalUrl, httpVersion } = request;
59 12
    const responseTime = Date.now() - res[startTime];
60 12
    const optionals = [
61
      res.statusCode,
62
      res.getHeader('content-length'),
63
      res.getHeader('content-type'),
64
      request.headers.referer,
65
      request.headers['user-agent'],
66
    ]
67 60
      .filter((i) => !!i)
68
      .join(' ');
69 12
    return `${remoteAddress} - "${method} ${originalUrl} HTTP/${httpVersion}" ${optionals} - ${responseTime}ms`;
70
  }
71
72
  /**
73
   * Initializes the application and checks
74
   * if all beans are valid
75
   * @param listen {boolean}
76
   * @param port {number}
77
   * @param beans {Object[]}
78
   * @param onInitialized {Function}
79
   * @private
80
   */
81 19
  private async initialize({
82
    listen = true,
83
    port = 8080,
84
    routerBeans = [],
85
  }: Partial<ExpressBeansOptions>) {
86 19
    return Promise.resolve(routerBeans as Array<ExpressRouterBean>)
87
      .then(this.checkRouterBeans.bind(this))
88
      .then(this.registerRouters.bind(this))
89
      .then(() => {
90 17
        if (listen) {
91 6
          return new Promise<void>(
92
            (resolve, reject) => {
93 6
              this.listen(port, (err) => (err ? reject(err) : resolve()));
94
            },
95 4
          ).catch((err) => Promise.reject(err));
96
        }
97 11
        return Promise.resolve();
98
      });
99
  }
100
101
  /**
102
   * Starts the server and emits the initialized event
103
   * @param {number} port
104
   */
105
  listen(port: number, callback?: (error?: Error) => void) {
106 16
    return this.app.listen(port, (error) => {
107 16
      callback?.(error);
108 16
      if (error) {
109 4
        this.emit('error', error);
110 1
        return;
111
      }
112 12
      logger.info(`Server listening on port ${port}`);
113 12
      this.emit('initialized');
114
    });
115
  }
116
117
  /**
118
   * Registers all routers
119
   * @param routers {Array<ExpressRouterBean>}
120
   * @private
121
   */
122
  private async registerRouters(routers: Array<ExpressRouterBean>) {
123 18
    return new Promise((resolve, reject) => {
124 18
      Array.from(routers)
125 16
        .map((bean) => bean._instance)
126
        .forEach((instance) => {
127 16
          try {
128
            const {
129
              path,
130
              router,
131 16
            } = instance._routerConfig;
132 16
            logger.debug(`Registering router ${instance._className}`);
133 16
            this.router.use(path, router);
134
          } catch (e) {
135 1
            logger.error(e);
136 1
            reject(new Error(`Router ${instance._className} not initialized correctly`, { cause: e }));
137
          }
138
        });
139 18
      resolve(true);
140
    });
141
  }
142
143
  /**
144
   * Checks if all beans are valid
145
   * @param routerBeans {Array<ExpressRouterBean>}
146
   * @returns {Array<ExpressRouterBean>}
147
   * @throws {Error}
148
   * @private
149
   */
150
  private async checkRouterBeans(routerBeans: Array<ExpressRouterBean>):
151
  Promise<ExpressRouterBean[]> {
152 19
    const invalidBeans = routerBeans
153 19
      .filter(((bean) => !bean._beanUUID))
154 1
      .map((object: any) => object.prototype.constructor.name);
155 19
    if (invalidBeans.length > 0) {
156 1
      return Promise.reject(new Error(`Trying to use something that is not an ExpressBean: ${invalidBeans.join(', ')}`));
157
    }
158 18
    return routerBeans;
159
  }
160
161
  /**
162
   * Gets Express application
163
   * @returns {Express}
164
   */
165
  getApp() {
166 2
    return this.app;
167
  }
168
169
  /**
170
   * Exposes use function of Express application
171
   * @param handlers
172
   */
173
  use(...handlers: any) {
174 1
    this.app.use(...handlers);
175
  }
176
}
177